﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

using Pg.BioMedics.SDR.Data;
using Pg.BioMedics.SDR.Data.Caching;
using Pg.BioMedics.SDR.Entities;
using Pg.BioMedics.SDR.Entities.Codes;
using Pg.BioMedics.SDR.Services.Thesaurus;
using Pg.BioMedics.SDR.Services.Algorithms;
using Pg.BioMedics.SDR.ServiceAgents;

namespace Pg.BioMedics.SDR.Services.Impl
{
    /// <summary>
    /// Provides methods for both spoken and textual queries processing against indexed
    /// spoken content descriptors.
    /// </summary>
    internal class SearchService : ISearchService
    {
        #region Constants
        
        private const string QueryAsrTaskPrefix = "SQ";
        private const int Interval = 1000;

        #endregion

        #region ISearchService Members

        /// <summary>
        /// Provides logic for textual query processing. This method matches given query
        /// against each index descriptor and calculates relavace score for this comperision.
        /// </summary>
        /// <param name="query">Set of textual terms (query)</param>
        /// <returns>Ranked list of results</returns>
        public IList<SearchResult> ExecuteTextQuery(string query)
        {
            // input params validation
            if (String.IsNullOrEmpty(query))
                throw new ServiceException("Couldnot execute text query, query cannot be null or empty string");

            try
            {
                string internalQuery = TextHelper.RemoveNonAlphaNumeric(query.ToLower());
                IList<Token> queryTokens = TokenHelper.ExtractTokens(internalQuery);

                // pre-process text query (as defined in the "Audio and Beyond Audio Content Indexing And Retrivial Page 136-137")                               
                queryTokens = TokenHelper.RemoveStopTokens(queryTokens);
                queryTokens = TokenHelper.ExpandWithSynonyms(queryTokens);
                queryTokens = TokenHelper.RemoveStopTokens(queryTokens);

                // calculate similarity measures between each active document descriptors
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    // precalculate algorithm initial values        
                    var documents = from document in dataContext.DocumentManager.GetAllDocuments()
                                    where (document.Id != null) && (document.StatusCode == DocumentStatusCodes.Created)
                                    select new { DocumentId = document.Id.Value };
                    var annotations = (from document in documents
                                       join annotation in dataContext.AnnotationManager.GetAllAnnotations() on document.DocumentId equals annotation.DocumentId
                                       select new { DocumentId = document.DocumentId, AnnotationId = annotation.Id });
                    var transcriptions = (from annotation in annotations
                                          let tokens = (from token in dataContext.AnnotationManager.GetAllTokens()
                                                        where token.AnnotationId == annotation.AnnotationId
                                                        select token).ToArray()
                                          where tokens.Length > 0
                                          select new { Tokens = tokens, AnnotationId = annotation.AnnotationId, DocumentId = annotation.DocumentId }).ToList();                    

                    // calculate mean transcription length
                    double meanLength = 0.0d;
                    for (int i = 0; i < transcriptions.Count; i++)
                        meanLength += transcriptions[i].Tokens.Length;

                    meanLength /= (double)transcriptions.Count;

                    // initializa Okapi algorithm
                    RSVOkapiAlgorithm rsvAlgorithm = new RSVOkapiAlgorithm(meanLength, transcriptions.Count(), 
                                                                           delegate(Token t)
                                                                           {
                                                                                int count = 0;
                                                                                for (int i = 0; i < transcriptions.Count; i++)
                                                                                    if (Array.IndexOf<Token>(transcriptions[i].Tokens, t) > -1)
                                                                                        count++;

                                                                                return count;
                                                                           });

                    // prepare ranking 
                    return (from transcription in transcriptions
                            let rsv = rsvAlgorithm.Calculate(queryTokens.ToArray(), transcription.Tokens)
                            where rsv > 0.0d
                            orderby rsv descending 
                            select new SearchResult() { DocumentId = transcription.DocumentId, RelevanceScore = rsv }).ToList();
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot execute text query {0}", ex, query);
            }
        }

        /// <summary>
        /// Provides logic for spoken query processing
        /// </summary>
        /// <param name="audioData">Spoken query audio data</param>
        /// <param name="timeout">Timeout in miliseconds, if -1 there is no timeout</param>
        /// <returns>Ranked list of results</returns>
        public IList<SearchResult> ExecuteSpokenQuery(byte[] audioData, int timeout)
        {
            try
            {
                IList<SearchResult> result = null;

                AutoResetEvent waitHandle = new AutoResetEvent(false);
                Thread worker = new Thread(delegate()
                {
                    // extract descriptor
                    string queryDescriptor = null;
                    using (ServiceAgentsContext serviceAgentsContext = new ServiceAgentsContext())
                    {
                        Guid taskUniqueId = serviceAgentsContext.ASRAgent.EnqueueTask(String.Format("{0}_{1}", QueryAsrTaskPrefix, Guid.NewGuid().ToString()),
                                                                                      audioData);
                        
                        do
                        {
                            Thread.Sleep(Interval);
                            if (serviceAgentsContext.ASRAgent.GetFinishedTasks().Contains(taskUniqueId))
                                queryDescriptor = serviceAgentsContext.ASRAgent.GetResult(taskUniqueId);
                        }
                        while (String.IsNullOrEmpty(queryDescriptor));
                    }

                    // extract tokens from the descriptor
                    IList<Token> queryTokens = Mpeg7Helper.ExtractTokens(queryDescriptor);

                    // pre-process text query (as defined in the "Audio and Beyond Audio Content Indexing And Retrivial Page 136-137")                               
                    queryTokens = TokenHelper.RemoveStopTokens(queryTokens);
                    queryTokens = TokenHelper.ExpandWithSynonyms(queryTokens);
                    queryTokens = TokenHelper.RemoveStopTokens(queryTokens);

                    // calculate similarity measures between each active document descriptors
                    using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                    {
                        // precalculate algorithm initial values        
                        var documents = from document in dataContext.DocumentManager.GetAllDocuments()
                                        where (document.Id != null) && (document.StatusCode == DocumentStatusCodes.Created)
                                        select new { DocumentId = document.Id.Value };
                        var annotations = (from document in documents
                                           join annotation in dataContext.AnnotationManager.GetAllAnnotations() on document.DocumentId equals annotation.DocumentId
                                           select new { DocumentId = document.DocumentId, AnnotationId = annotation.Id });
                        var transcriptions = (from annotation in annotations
                                              let tokens = (from token in dataContext.AnnotationManager.GetAllTokens()
                                                            where token.AnnotationId == annotation.AnnotationId
                                                            select token).ToArray()
                                              where tokens.Length > 0
                                              select new { Tokens = tokens, AnnotationId = annotation.AnnotationId, DocumentId = annotation.DocumentId }).ToList();

                        // calculate mean transcription length
                        double meanLength = 0.0d;
                        for (int i = 0; i < transcriptions.Count; i++)
                            meanLength += transcriptions[i].Tokens.Length;

                        meanLength /= (double)transcriptions.Count;

                        // initializa Okapi algorithm
                        RSVOkapiAlgorithm rsvAlgorithm = new RSVOkapiAlgorithm(meanLength, transcriptions.Count(),
                                                                               delegate(Token t)
                                                                               {
                                                                                   int count = 0;
                                                                                   for (int i = 0; i < transcriptions.Count; i++)
                                                                                       if (Array.IndexOf<Token>(transcriptions[i].Tokens, t) > -1)
                                                                                           count++;

                                                                                   return count;
                                                                               });

                        // prepare ranking 
                        result =  (from transcription in transcriptions
                                   let rsv = rsvAlgorithm.Calculate(queryTokens.ToArray(), transcription.Tokens)
                                   where rsv > 0.0d
                                   orderby rsv descending
                                   select new SearchResult() { DocumentId = transcription.DocumentId, RelevanceScore = rsv }).ToList();
                    }

                    // set wait handle
                    waitHandle.Set();
                });
                worker.Start();

                // wait for operation completion or timeout
                waitHandle.WaitOne(timeout, true);

                // check timeout
                if (worker.IsAlive)
                {
                    worker.Abort();
                    throw new ServiceException("Timeout elapsed");
                }

                // return result
                return result;
            }
            catch (Exception ex)
            {
                throw new ServiceException("Couldnot execute spoken query", ex);
            }
        }

        /// <summary>
        /// Provides logic for Document extraction form the search result
        /// </summary>
        /// <param name="searchResult">Valid search result</param>
        /// <returns>Extracted document</returns>
        public Document GetDocument(SearchResult searchResult)
        {
            // input params validation
            if (searchResult == null)
                throw new ServiceException("Could not get document, search result cannot be null");
            else if (searchResult.DocumentId < 1)
                throw new ServiceException("Could not get document, search result document value {0} is invalid", searchResult.DocumentId);

            try
            {
                using (SDRDataContext dataContext = new SDRCachedDataContext(false))
                {
                    return dataContext.DocumentManager.GetDocumentById(searchResult.DocumentId);
                }
            }
            catch (Exception ex)
            {
                throw new ServiceException("Could not get document for the search result {0} {1}", ex, searchResult.DocumentId, searchResult.RelevanceScore);
            }
        }

        #endregion
    }
}
